Skip to content

S01-25 JavaSE-数据库-JDBC

[TOC]

JDBC概述

基本介绍

  1. JDBC(Java Database Connectivity):Java访问数据库的统一接口
  2. 作用:屏蔽不同数据库的实现细节,Java程序通过JDBC驱动访问任意数据库
  3. 核心:Java提供接口,数据库厂商提供驱动(jar包)实现

模拟JDBC实现

java
// JDBC接口(Java制定规范)
public interface JdbcInterface {
    Object getConnection();
    void crud();
    void close();
}

// MySQL驱动实现
public class MysqlJdbcImpl implements JdbcInterface {
    @Override
    public Object getConnection() {
        System.out.println("得到mysql的连接");
        return null;
    }

    @Override
    public void crud() {
        System.out.println("完成mysql增删改查");
    }

    @Override
    public void close() {
        System.out.println("关闭mysql的连接");
    }
}

// Oracle驱动实现
public class OracleJdbcImpl implements JdbcInterface {
    @Override
    public Object getConnection() {
        System.out.println("得到oracle的连接");
        return null;
    }

    @Override
    public void crud() {
        System.out.println("完成oracle增删改查");
    }

    @Override
    public void close() {
        System.out.println("关闭oracle的连接");
    }
}

// 测试类
public class TestJDBC {
    public static void main(String[] args) {
        // 访问MySQL
        JdbcInterface jdbc = new MysqlJdbcImpl();
        jdbc.getConnection();
        jdbc.crud();
        jdbc.close();

        // 访问Oracle
        jdbc = new OracleJdbcImpl();
        jdbc.getConnection();
        jdbc.crud();
        jdbc.close();
    }
}

JDBC快速入门

JDBC程序编写步骤

  1. 注册驱动(加载Driver类)
  2. 获取连接(Connection)
  3. 执行SQL(Statement/PreparedStatement)
  4. 释放资源(关闭连接、语句、结果集)

第一个JDBC程序(操作actor表)

java
import com.mysql.jdbc.Driver;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Properties;

public class Jdbc01 {
    public static void main(String[] args) throws Exception {
        // 1. 注册驱动
        Driver driver = new Driver();

        // 2. 获取连接
        String url = "jdbc:mysql://localhost:3306/hsp_db02";
        Properties props = new Properties();
        props.setProperty("user", "root");
        props.setProperty("password", "hsp");
        Connection conn = driver.connect(url, props);

        // 3. 执行SQL(增删改)
        String sql = "insert into actor values(null, '刘德华', '男', '1970-11-11', '110')";
        // String sql = "update actor set name='周星驰' where id = 1";
        // String sql = "delete from actor where id = 1";
        Statement stmt = conn.createStatement();
        int rows = stmt.executeUpdate(sql); // DML返回影响行数
        System.out.println(rows > 0 ? "成功" : "失败");

        // 4. 释放资源
        stmt.close();
        conn.close();
    }
}

获取数据库连接5种方式

方式1:直接创建Driver对象

java
@Test
public void connect01() throws Exception {
    Driver driver = new com.mysql.jdbc.Driver();
    String url = "jdbc:mysql://localhost:3306/hsp_db02";
    Properties props = new Properties();
    props.setProperty("user", "root");
    props.setProperty("password", "hsp");
    Connection conn = driver.connect(url, props);
    System.out.println(conn);
}

方式2:反射加载Driver(动态加载)

java
@Test
public void connect02() throws Exception {
    Class<?> clazz = Class.forName("com.mysql.jdbc.Driver");
    Driver driver = (Driver) clazz.newInstance();
    String url = "jdbc:mysql://localhost:3306/hsp_db02";
    Properties props = new Properties();
    props.setProperty("user", "root");
    props.setProperty("password", "hsp");
    Connection conn = driver.connect(url, props);
    System.out.println("方式2=" + conn);
}

方式3:使用DriverManager管理驱动

java
@Test
public void connect03() throws Exception {
    Class<?> clazz = Class.forName("com.mysql.jdbc.Driver");
    Driver driver = (Driver) clazz.newInstance();
    String url = "jdbc:mysql://localhost:3306/hsp_db02";
    String user = "root";
    String pwd = "hsp";
    DriverManager.registerDriver(driver);
    Connection conn = DriverManager.getConnection(url, user, pwd);
    System.out.println("方式3=" + conn);
}

方式4:Class.forName自动注册驱动(推荐)

java
@Test
public void connect04() throws Exception {
    // 加载Driver类时,静态代码块自动注册驱动(DriverManager.registerDriver)
    Class.forName("com.mysql.jdbc.Driver");
    String url = "jdbc:mysql://localhost:3306/hsp_db02";
    String user = "root";
    String pwd = "hsp";
    Connection conn = DriverManager.getConnection(url, user, pwd);
    System.out.println("方式4=" + conn);
}

方式5:配置文件+反射(最灵活)

  1. 创建src/mysql.properties配置文件:
properties
user=root
password=hsp
url=jdbc:mysql://localhost:3306/hsp_db02
driver=com.mysql.jdbc.Driver
  1. Java代码:
java
@Test
public void connect05() throws Exception {
    // 加载配置文件
    Properties props = new Properties();
    props.load(new FileInputStream("src/mysql.properties"));
    String user = props.getProperty("user");
    String pwd = props.getProperty("password");
    String url = props.getProperty("url");
    String driver = props.getProperty("driver");

    Class.forName(driver); // 推荐写上
    Connection conn = DriverManager.getConnection(url, user, pwd);
    System.out.println("方式5=" + conn);
}

ResultSet(结果集)

基本介绍

  • 存储查询结果的数据集,光标初始在第一行之前
  • next():光标下移一行,返回false表示无更多行
  • 支持通过列索引或列名获取值

应用实例

java
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
import java.io.FileInputStream;

public class ResultSet_ {
    public static void main(String[] args) throws Exception {
        // 加载配置
        Properties props = new Properties();
        props.load(new FileInputStream("src/mysql.properties"));
        String user = props.getProperty("user");
        String pwd = props.getProperty("password");
        String url = props.getProperty("url");
        String driver = props.getProperty("driver");

        // 1. 注册驱动
        Class.forName(driver);

        // 2. 获取连接
        Connection conn = DriverManager.getConnection(url, user, pwd);

        // 3. 创建Statement
        Statement stmt = conn.createStatement();

        // 4. 执行查询
        String sql = "select id, name, sex, borndate from actor";
        ResultSet rs = stmt.executeQuery(sql);

        // 5. 遍历结果集
        while (rs.next()) {
            int id = rs.getInt("id"); // 列名(推荐)
            String name = rs.getString("name");
            String sex = rs.getString("sex");
            java.sql.Date borndate = rs.getDate("borndate");
            System.out.println(id + "\t" + name + "\t" + sex + "\t" + borndate);
        }

        // 6. 释放资源
        rs.close();
        stmt.close();
        conn.close();
    }
}

Statement(SQL注入问题)

问题说明

  • Statement执行静态SQL,通过字符串拼接生成SQL,存在SQL注入风险
  • SQL注入:恶意用户输入非法SQL片段,改变原SQL逻辑

演示SQL注入

java
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
import java.util.Scanner;
import java.io.FileInputStream;

public class Statement_ {
    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入管理员名字:");
        String name = scanner.nextLine();
        System.out.print("请输入密码:");
        String pwd = scanner.nextLine();

        // 加载配置
        Properties props = new Properties();
        props.load(new FileInputStream("src/mysql.properties"));
        String user = props.getProperty("user");
        String password = props.getProperty("password");
        String url = props.getProperty("url");
        String driver = props.getProperty("driver");

        // 连接数据库
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);
        Statement stmt = conn.createStatement();

        // 拼接SQL(存在注入风险)
        String sql = "select name, pwd from admin where name = '" + name + "' and pwd = '" + pwd + "'";
        ResultSet rs = stmt.executeQuery(sql);

        // 判断登录结果
        if (rs.next()) {
            System.out.println("登录成功");
        } else {
            System.out.println("登录失败");
        }

        // 释放资源
        rs.close();
        stmt.close();
        conn.close();
    }
}

注入测试

  • 用户名输入:1' or
  • 密码输入:or '1'='1
  • 拼接后的SQL:select name, pwd from admin where name = '1' or' and pwd = 'or '1'='1'
  • 结果:无需正确密码即可登录

PreparedStatement(预处理,解决SQL注入)

基本介绍

  • 预编译SQL,参数用?占位符
  • 通过setXxx(索引, 值)设置参数(索引从1开始)
  • 优点:避免SQL注入、减少编译次数、语法简洁

应用实例(登录功能)

java
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;
import java.util.Scanner;
import java.io.FileInputStream;

public class PreparedStatement_ {
    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入管理员名字:");
        String name = scanner.nextLine();
        System.out.print("请输入密码:");
        String pwd = scanner.nextLine();

        // 加载配置
        Properties props = new Properties();
        props.load(new FileInputStream("src/mysql.properties"));
        String user = props.getProperty("user");
        String password = props.getProperty("password");
        String url = props.getProperty("url");
        String driver = props.getProperty("driver");

        // 连接数据库
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);

        // 预处理SQL(?占位符)
        String sql = "select name, pwd from admin where name = ? and pwd = ?";
        PreparedStatement pstmt = conn.prepareStatement(sql);

        // 设置参数(索引1对应第一个?,索引2对应第二个?)
        pstmt.setString(1, name);
        pstmt.setString(2, pwd);

        // 执行查询(无需传入SQL)
        ResultSet rs = pstmt.executeQuery();

        // 判断登录结果
        if (rs.next()) {
            System.out.println("登录成功");
        } else {
            System.out.println("登录失败");
        }

        // 释放资源
        rs.close();
        pstmt.close();
        conn.close();
    }
}

PreparedStatement执行DML(增删改)

java
public class PreparedStatementDML_ {
    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入要删除的管理员名字:");
        String name = scanner.nextLine();

        // 加载配置
        Properties props = new Properties();
        props.load(new FileInputStream("src/mysql.properties"));
        String user = props.getProperty("user");
        String password = props.getProperty("password");
        String url = props.getProperty("url");
        String driver = props.getProperty("driver");

        // 连接数据库
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);

        // 预处理SQL(删除)
        String sql = "delete from admin where name = ?";
        PreparedStatement pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, name);

        // 执行DML(返回影响行数)
        int rows = pstmt.executeUpdate();
        System.out.println(rows > 0 ? "执行成功" : "执行失败");

        // 释放资源
        pstmt.close();
        conn.close();
    }
}

JDBC API小结

组件核心方法
DriverManagergetConnection(url, user, pwd):获取连接
ConnectioncreateStatement():创建Statement
prepareStatement(sql):创建PreparedStatement
StatementexecuteUpdate(sql):执行DML
executeQuery(sql):执行查询
execute(sql):执行任意SQL
PreparedStatementsetXxx(索引, 值):设置参数
executeUpdate():执行DML
executeQuery():执行查询
ResultSetnext():光标下移
getXxx(列名/索引):获取值
getObject(列名/索引):获取任意类型值

封装JDBCUtils工具类

工具类代码

java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
import java.io.FileInputStream;
import java.io.IOException;

public class JDBCUtils {
    // 静态属性(只加载一次)
    private static String user;
    private static String password;
    private static String url;
    private static String driver;

    // 静态代码块初始化配置
    static {
        try {
            Properties props = new Properties();
            props.load(new FileInputStream("src/mysql.properties"));
            user = props.getProperty("user");
            password = props.getProperty("password");
            url = props.getProperty("url");
            driver = props.getProperty("driver");
            // 注册驱动
            Class.forName(driver);
        } catch (IOException | ClassNotFoundException e) {
            // 编译异常转运行异常
            throw new RuntimeException("初始化JDBC工具类失败", e);
        }
    }

    // 获取连接
    public static Connection getConnection() throws Exception {
        return DriverManager.getConnection(url, user, password);
    }

    // 释放资源
    public static void close(ResultSet rs, Statement stmt, Connection conn) {
        try {
            if (rs != null) rs.close();
            if (stmt != null) stmt.close();
            if (conn != null) conn.close();
        } catch (Exception e) {
            throw new RuntimeException("释放资源失败", e);
        }
    }

    // 重载(无ResultSet时)
    public static void close(Statement stmt, Connection conn) {
        close(null, stmt, conn);
    }
}

工具类使用示例

java
public class TestJDBCUtils {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            // 获取连接
            conn = JDBCUtils.getConnection();

            // 预处理SQL
            String sql = "select * from actor where id = ?";
            pstmt = conn.prepareStatement(sql);
            pstmt.setInt(1, 1);

            // 执行查询
            rs = pstmt.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getString("name") + "\t" + rs.getString("phone"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            JDBCUtils.close(rs, pstmt, conn);
        }
    }
}

第25章 JDBC进阶

事务

基本介绍

  1. JDBC程序中当一个Connection对象创建时,默认情况下是自动提交事务:每次执行一个SQL语句时,如果执行成功,就会向数据库自动提交,而不能回滚。
  2. JDBC程序中为了让多个SQL语句作为一个整体执行,需要使用事务。
  3. 调用ConnectionsetAutoCommit(false)可以取消自动提交事务。
  4. 在所有的SQL语句都成功执行后,调用Connectioncommit();方法提交事务。
  5. 在其中某个操作失败或出现异常时,调用Connectionrollback();方法回滚事务。

应用实例

模拟经典的转账业务

sql
create table account(
    id int primary key auto_increment,
    name varchar(32) not null default '',
    balance double not null default 0
) character set utf8;

insert into account values(null,'马化腾',10000);
insert into account values(null,'马云',3000);

不使用事务可能出现的问题模拟

模拟经典的转账业务(转账过程中出现异常导致数据不一致)

使用事务解决上述问题-模拟经典的转账业务

java
package com.hspedu.jdbc.transaction_;

import com.hspedu.jdbc.utils.JDBCUtils;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

/**
 * @author 韩顺平
 * @version 1.0
 * 演示jdbc 中如何使用事务
 */
public class Transaction_ {

    //没有使用事务
    @Test
    public void noTransaction() {
        //操作转账的业务
        //1. 得到连接
        Connection connection = null;
        //2. 组织一个sql
        String sql = "update account set balance = balance - 100 where id = 1";
        String sql2 = "update account set balance = balance + 100 where id = 2";
        PreparedStatement preparedStatement = null;

        try {
            connection = JDBCUtils.getConnection(); // 在默认情况下,connection 是默认自动提交
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.executeUpdate(); // 执行第1 条sql
            int i = 1 / 0; //抛出异常
            preparedStatement = connection.prepareStatement(sql2);
            preparedStatement.executeUpdate(); // 执行第2 条sql
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            //关闭资源
            JDBCUtils.close(null, preparedStatement, connection);
        }
    }

    //使用事务来解决
    @Test
    public void useTransaction() {
        //操作转账的业务
        //1. 得到连接
        Connection connection = null;
        //2. 组织一个sql
        String sql = "update account set balance = balance - 100 where id = 1";
        String sql2 = "update account set balance = balance + 100 where id = 2";
        PreparedStatement preparedStatement = null;

        try {
            connection = JDBCUtils.getConnection(); // 在默认情况下,connection 是默认自动提交
            //将connection 设置为不自动提交
            connection.setAutoCommit(false); //开启了事务
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.executeUpdate(); // 执行第1 条sql
            int i = 1 / 0; //抛出异常
            preparedStatement = connection.prepareStatement(sql2);
            preparedStatement.executeUpdate(); // 执行第2 条sql
            //这里提交事务
            connection.commit();
        } catch (SQLException e) {
            //这里我们可以进行回滚,即撤销执行的SQL
            //默认回滚到事务开始的状态
            System.out.println("执行发生了异常,撤销执行的sql");
            try {
                connection.rollback();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            //关闭资源
            JDBCUtils.close(null, preparedStatement, connection);
        }
    }
}

课后练习

参考老师代码:

  1. 创建account
  2. 在表中先添加两条记录:tom余额100,king余额200
  3. 使用事务完成,tom给king转10元钱。

批处理

基本介绍

  1. 当需要成批插入或者更新记录时,可以采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理,通常情况下比单独提交处理更有效率。
  2. JDBC的批量处理语句包括下面方法:
    • addBatch():添加需要批量处理的SQL语句或参数
    • executeBatch():执行批量处理语句
    • clearBatch():清空批处理包的语句
  3. JDBC连接MySQL时,如果要使用批处理功能,请在url中加参数?rewriteBatchedStatements=true
  4. 批处理往往和PreparedStatement一起搭配使用,可以既减少编译次数,又减少运行次数,效率大大提高。

应用实例

Batch_.java:演示向admin2表中添加5000条数据,对比传统方式和批量方式的耗时

注意:需要修改配置文件jdbc.properties

properties
url=jdbc:mysql://localhost:3306/数据库?rewriteBatchedStatements=true

创建表:

sql
create table admin2(
    id int primary key auto_increment,
    username varchar(32) not null,
    password varchar(32) not null
);

Java代码:

java
package com.hspedu.jdbc.batch_;

import com.hspedu.jdbc.utils.JDBCUtils;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

/**
 * @author 韩顺平
 * @version 1.0
 * 演示java 的批处理
 */
public class Batch_ {

    //传统方法,添加5000 条数据到admin2
    @Test
    public void noBatch() throws Exception {
        Connection connection = JDBCUtils.getConnection();
        String sql = "insert into admin2 values(null, ?, ?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        System.out.println("开始执行");
        long start = System.currentTimeMillis();//开始时间
        for (int i = 0; i < 5000; i++) {//5000 次执行
            preparedStatement.setString(1, "jack" + i);
            preparedStatement.setString(2, "666");
            preparedStatement.executeUpdate();
        }
        long end = System.currentTimeMillis();
        System.out.println("传统的方式耗时=" + (end - start));//传统的方式耗时=10702
        //关闭连接
        JDBCUtils.close(null, preparedStatement, connection);
    }

    //使用批量方式添加数据
    @Test
    public void batch() throws Exception {
        Connection connection = JDBCUtils.getConnection();
        String sql = "insert into admin2 values(null, ?, ?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        System.out.println("开始执行");
        long start = System.currentTimeMillis();//开始时间
        for (int i = 0; i < 5000; i++) {//5000 次执行
            preparedStatement.setString(1, "jack" + i);
            preparedStatement.setString(2, "666");
            //将sql 语句加入到批处理包中-> 看源码
            /*
            //1. 第一次就创建ArrayList - elementData => Object[]
            //2. elementData => Object[] 就会存放我们预处理的sql 语句
            //3. 当elementData 满后,就按照1.5 扩容
            //4. 当添加到指定的值后,就executeBatch
            //5. 批量处理会减少我们发送sql 语句的网络开销,而且减少编译次数,因此效率提高
            public void addBatch() throws SQLException {
                synchronized(this.checkClosed().getConnectionMutex()) {
                    if (this.batchedArgs == null) {
                        this.batchedArgs = new ArrayList();
                    }
                    for(int i = 0; i < this.parameterValues.length; ++i) {
                        this.checkAllParametersSet(this.parameterValues[i], this.parameterStreams[i], i);
                    }
                    this.batchedArgs.add(new PreparedStatement.BatchParams(this.parameterValues, this.parameterStreams, this.isStream, this.streamLengths, this.isNull));
                }
            }
            */
            preparedStatement.addBatch();
            //当有1000 条记录时,批量执行一次
            if((i + 1) % 1000 == 0) {//满1000 条sql
                preparedStatement.executeBatch();
                //清空批处理包
                preparedStatement.clearBatch();
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("批量方式耗时=" + (end - start));//批量方式耗时=108
        //关闭连接
        JDBCUtils.close(null, preparedStatement, connection);
    }
}

数据库连接池

5k次连接数据库问题

ConQuestion.java:编写程序完成连接MySQL5000次的操作,观察耗时和问题

java
package com.hspedu.jdbc.datasource;

import com.hspedu.jdbc.utils.JDBCUtils;
import org.junit.jupiter.api.Test;
import java.sql.Connection;

/**
 * @author 韩顺平
 * @version 1.0
 */
public class ConQuestion {

    //代码连接mysql 5000 次
    @Test
    public void testCon() {
        //看看连接-关闭connection 会耗用多久
        long start = System.currentTimeMillis();
        System.out.println("开始连接.....");
        for (int i = 0; i < 5000; i++) {
            //使用传统的jdbc 方式,得到连接
            Connection connection = JDBCUtils.getConnection();
            //做一些工作,比如得到PreparedStatement,发送sql
            //关闭连接
            JDBCUtils.close(null, null, connection);
        }
        long end = System.currentTimeMillis();
        System.out.println("传统方式5000 次耗时=" + (end - start));//传统方式5000 次耗时=7099
    }
}

传统获取Connection问题分析

  1. 传统的JDBC数据库连接使用DriverManager来获取,每次向数据库建立连接的时候都要将Connection加载到内存中,再验证IP地址、用户名和密码(0.05s~1s时间)。
  2. 需要数据库连接的时候,就向数据库要求一个,频繁的进行数据库连接操作将占用很多的系统资源,容易造成服务器崩溃。
  3. 每一次数据库连接,使用完后都得断开,如果程序出现异常而未能关闭,将导致数据库内存泄漏,最终将导致重启数据库。
  4. 传统获取连接的方式,不能控制创建的连接数量,如连接过多,也可能导致内存泄漏,MySQL崩溃。

解决方式:采用数据库连接池技术(connection pool)

数据库连接池种类

JDBC的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由第三方提供实现(需导入对应.jar包):

  • C3P0数据库连接池:速度相对较慢,稳定性不错(hibernate、spring中常用)
  • DBCP数据库连接池:速度相对c3p0较快,但不稳定
  • Proxool数据库连接池:有监控连接池状态的功能,稳定性较c3p0差一点
  • BoneCP数据库连接池:速度快
  • Druid(德鲁伊):阿里提供的数据库连接池,集DBCP、C3P0、Proxool优点于一身

C3P0应用实例

使用代码实现c3p0数据库连接池,配置文件放src目录下

java
package com.hspedu.jdbc.datasource;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.junit.jupiter.api.Test;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

/**
 * @author 韩顺平
 * @version 1.0
 * 演示c3p0 的使用
 */
public class C3P0_ {

    //方式1: 相关参数在程序中指定
    @Test
    public void testC3P0_01() throws Exception {
        //1. 创建一个数据源对象
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        //2. 通过配置文件mysql.properties 获取相关连接的信息
        Properties properties = new Properties();
        properties.load(new FileInputStream("src\\mysql.properties"));
        //读取相关的属性值
        String user = properties.getProperty("user");
        String password = properties.getProperty("password");
        String url = properties.getProperty("url");
        String driver = properties.getProperty("driver");

        //给数据源comboPooledDataSource 设置相关的参数
        //注意: 连接管理是由comboPooledDataSource 来管理
        comboPooledDataSource.setDriverClass(driver);
        comboPooledDataSource.setJdbcUrl(url);
        comboPooledDataSource.setUser(user);
        comboPooledDataSource.setPassword(password);

        //设置连接池参数
        comboPooledDataSource.setInitialPoolSize(10); //初始化连接数
        comboPooledDataSource.setMaxPoolSize(50); //最大连接数

        //测试连接池的效率, 测试对mysql 5000 次操作
        long start = System.currentTimeMillis();
        for (int i = 0; i < 5000; i++) {
            Connection connection = comboPooledDataSource.getConnection(); //从连接池获取连接
            //System.out.println("连接OK");
            connection.close(); //关闭连接(实际是放回连接池)
        }
        long end = System.currentTimeMillis();
        System.out.println("c3p0 5000 连接mysql 耗时=" + (end - start));//c3p0 5000 连接mysql 耗时=391
    }

    //第二种方式: 使用配置文件模板来完成
    @Test
    public void testC3P0_02() throws SQLException {
        //1. 将c3p0 提供的c3p0-config.xml 拷贝到src 目录下
        //2. 该文件指定了连接数据库和连接池的相关参数
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource("hsp_edu");

        //测试500000次连接mysql
        long start = System.currentTimeMillis();
        System.out.println("开始执行....");
        for (int i = 0; i < 500000; i++) {
            Connection connection = comboPooledDataSource.getConnection();
            //System.out.println("连接OK~");
            connection.close();
        }
        long end = System.currentTimeMillis();
        System.out.println("c3p0 的第二种方式(500000次) 耗时=" + (end - start));//约1917ms
    }
}

Druid(德鲁伊)应用实例

使用代码实现Druid数据库连接池

java
package com.hspedu.jdbc.datasource;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.io.FileInputStream;
import java.sql.Connection;
import java.util.Properties;

/**
 * @author 韩顺平
 * @version 1.0
 * 测试druid的使用
 */
public class Druid_ {

    @Test
    public void testDruid() throws Exception {
        //1. 加入Druid jar 包
        //2. 加入配置文件druid.properties , 将该文件拷贝到项目的src 目录
        //3. 创建Properties 对象, 读取配置文件
        Properties properties = new Properties();
        properties.load(new FileInputStream("src\\druid.properties"));

        //4. 创建一个指定参数的数据库连接池
        DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);

        //测试连接池效率(500000次连接)
        long start = System.currentTimeMillis();
        for (int i = 0; i < 500000; i++) {
            Connection connection = dataSource.getConnection();
            //System.out.println("连接成功!");
            connection.close(); //放回连接池
        }
        long end = System.currentTimeMillis();
        System.out.println("druid 连接池操作500000次耗时=" + (end - start));//约539ms
    }
}

将JDBCUtils工具类改成Druid(德鲁伊)实现

基于Druid数据库连接池的工具类

java
package com.hspedu.jdbc.datasource;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

/**
 * @author 韩顺平
 * @version 1.0
 * 基于druid 数据库连接池的工具类
 */
public class JDBCUtilsByDruid {

    private static DataSource ds;

    //在静态代码块完成ds 初始化
    static {
        Properties properties = new Properties();
        try {
            properties.load(new FileInputStream("src\\druid.properties"));
            ds = DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //编写getConnection 方法
    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }

    //关闭连接, 注意: 在数据库连接池技术中,close 不是真的断掉连接
    //而是把使用的Connection 对象放回连接池
    public static void close(ResultSet resultSet, Statement statement, Connection connection) {
        try {
            if (resultSet != null) {
                resultSet.close();
            }
            if (statement != null) {
                statement.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

工具类使用示例:

java
package com.hspedu.jdbc.datasource;

import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;

/**
 * @author 韩顺平
 * @version 1.0
 */
@SuppressWarnings({"all"})
public class JDBCUtilsByDruid_USE {

    @Test
    public void testSelect() {
        System.out.println("使用druid 方式完成查询");
        //1. 得到连接
        Connection connection = null;
        //2. 组织一个sql
        String sql = "select * from actor where id >= ?";
        PreparedStatement preparedStatement = null;
        ResultSet set = null;

        try {
            connection = JDBCUtilsByDruid.getConnection();
            System.out.println(connection.getClass());//运行类型com.alibaba.druid.pool.DruidPooledConnection
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, 1);//给?号赋值
            //执行, 得到结果集
            set = preparedStatement.executeQuery();
            //遍历该结果集
            while (set.next()) {
                int id = set.getInt("id");
                String name = set.getString("name");
                String sex = set.getString("sex");
                Date borndate = set.getDate("borndate");
                String phone = set.getString("phone");
                System.out.println(id + "\t" + name + "\t" + sex + "\t" + borndate + "\t" + phone);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            //关闭资源
            JDBCUtilsByDruid.close(set, preparedStatement, connection);
        }
    }

    //将ResultSet 封装到ArrayList<Actor>中
    @Test
    public ArrayList<Actor> testSelectToArrayList() {
        System.out.println("使用druid 方式完成查询并封装");
        //1. 得到连接
        Connection connection = null;
        //2. 组织一个sql
        String sql = "select * from actor where id >= ?";
        PreparedStatement preparedStatement = null;
        ResultSet set = null;
        ArrayList<Actor> list = new ArrayList<>();//创建ArrayList 对象,存放actor 对象

        try {
            connection = JDBCUtilsByDruid.getConnection();
            System.out.println(connection.getClass());//运行类型com.alibaba.druid.pool.DruidPooledConnection
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, 1);//给?号赋值
            //执行, 得到结果集
            set = preparedStatement.executeQuery();
            //遍历该结果集
            while (set.next()) {
                int id = set.getInt("id");
                String name = set.getString("name");
                String sex = set.getString("sex");
                Date borndate = set.getDate("borndate");
                String phone = set.getString("phone");
                //把得到的resultset 的记录,封装到Actor 对象,放入到list 集合
                list.add(new Actor(id, name, sex, borndate, phone));
            }
            System.out.println("list 集合数据=" + list);
            for(Actor actor : list) {
                System.out.println("id=" + actor.getId() + "\t" + actor.getName());
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            //关闭资源
            JDBCUtilsByDruid.close(set, preparedStatement, connection);
        }
        //因为ArrayList 和connection 没有任何关联,所以该集合可以复用
        return list;
    }
}

Apache-DBUtils

问题分析

  1. 关闭connection后,resultSet结果集无法使用。
  2. resultSet不利于数据的管理(只能用一次,数据结构不直观)。
  3. 数据复用性差,无法直接将结果集转换为业务对象。

解决方案:将结果集记录封装到ArrayList<Actor>中,一个Actor对象对应一条actor表记录。

自定义封装方式(土方法)

参考JDBCUtilsByDruid_USE中的testSelectToArrayList方法。

基本介绍

commons-dbutils是Apache组织提供的一个开源JDBC工具类库,它是对JDBC的封装,使用dbutils能极大简化jdbc编码的工作量。

核心组件:

  1. DbUtils类:提供数据库连接相关的工具方法
  2. QueryRunner类:封装了SQL的执行,是线程安全的,可实现增、删、改、查、批处理
  3. ResultSetHandler接口:用于处理java.sql.ResultSet,将数据按要求转换为另一种形式

常用ResultSetHandler实现类:

实现类功能描述
ArrayHandler把结果集中的第一行数据转成对象数组
ArrayListHandler把结果集中的每一行数据都转成一个数组,再存放到List中
BeanHandler将结果集中的第一行数据封装到一个对应的JavaBean实例中
BeanListHandler将结果集中的每一行数据都封装到一个对应的JavaBean实例中,存放到List里
ColumnListHandler将结果集中某一列的数据存放到List中
KeyedHandler(name)将结果集中的每行数据都封装到Map里,再把这些map存到一个map里,其key为指定的列名
MapHandler将结果集中的第一行数据封装到一个Map里,key是列名,value是对应的值
MapListHandler将结果集中的每一行数据都封装到一个Map里,然后再存放到List

应用实例

使用DBUtils + Druid方式,完成对actor表的CRUD操作

java
package com.hspedu.jdbc.datasource;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.util.List;

/**
 * @author 韩顺平
 * @version 1.0
 * 使用apache-DBUtils 工具类+ druid 完成对表的crud 操作
 */
@SuppressWarnings({"all"})
public class DBUtils_USE {

    //查询多行记录
    @Test
    public void testQueryMany() throws SQLException {
        //1. 得到连接(druid)
        Connection connection = JDBCUtilsByDruid.getConnection();
        //2. 创建QueryRunner
        QueryRunner queryRunner = new QueryRunner();
        //3. 组织sql
        String sql = "select id, name from actor where id >= ?";
        //4. 执行查询,得到结果集(封装为Actor列表)
        /*
        老韩解读:
        (1) query 方法执行sql 语句,得到resultset 并封装到ArrayList 集合中
        (2) connection: 数据库连接
        (3) sql: 执行的sql 语句
        (4) new BeanListHandler<>(Actor.class): 将resultset -> Actor 对象-> 封装到ArrayList
            底层使用反射机制获取Actor 类的属性,然后进行封装
        (5) 1: 给sql 语句中的? 赋值(可变参数)
        (6) 底层会自动关闭resultset 和PreparedStatement
         */
        List<Actor> list = queryRunner.query(connection, sql, new BeanListHandler<>(Actor.class), 1);
        System.out.println("===查询结果===");
        for (Actor actor : list) {
            System.out.println(actor);
        }
        //释放资源(只需要关闭连接,因为DBUtils已关闭resultset和statement)
        JDBCUtilsByDruid.close(null, null, connection);
    }

    //查询单行记录(单个对象)
    @Test
    public void testQuerySingle() throws SQLException {
        //1. 得到连接(druid)
        Connection connection = JDBCUtilsByDruid.getConnection();
        //2. 创建QueryRunner
        QueryRunner queryRunner = new QueryRunner();
        //3. 组织sql
        String sql = "select * from actor where id = ?";
        //4. 执行查询(BeanHandler用于封装单行记录)
        Actor actor = queryRunner.query(connection, sql, new BeanHandler<>(Actor.class), 6);
        System.out.println("====查询单行结果====");
        System.out.println(actor);
        //释放资源
        JDBCUtilsByDruid.close(null, null, connection);
    }

    //查询单行单列(返回单个值)
    @Test
    public void testScalar() throws SQLException {
        //1. 得到连接(druid)
        Connection connection = JDBCUtilsByDruid.getConnection();
        //2. 创建QueryRunner
        QueryRunner queryRunner = new QueryRunner();
        //3. 组织sql
        String sql = "select name from actor where id = ?";
        //4. 执行查询(ScalarHandler用于封装单行单列值)
        Object obj = queryRunner.query(connection, sql, new ScalarHandler(), 4);
        System.out.println("====查询单行单列值===");
        System.out.println(obj);
        //释放资源
        JDBCUtilsByDruid.close(null, null, connection);
    }

    //执行dml操作(insert, update, delete)
    @Test
    public void testDML() throws SQLException {
        //1. 得到连接(druid)
        Connection connection = JDBCUtilsByDruid.getConnection();
        //2. 创建QueryRunner
        QueryRunner queryRunner = new QueryRunner();
        //3. 组织sql(可替换为insert、update、delete)
        //String sql = "update actor set name = ? where id = ?";
        //String sql = "insert into actor values(null, ?, ?, ?, ?)";
        String sql = "delete from actor where id = ?";

        //4. 执行dml操作(update方法返回受影响的行数)
        //int affectedRow = queryRunner.update(connection, sql, "林青霞", "女", "1966-10-10", "116");
        int affectedRow = queryRunner.update(connection, sql, 1000);
        System.out.println(affectedRow > 0 ? "执行成功" : "执行没有影响到表");

        //释放资源
        JDBCUtilsByDruid.close(null, null, connection);
    }
}

表和JavaBean的类型映射关系

数据库类型Java类型说明
int(11)Integer包装类可接收null值
varchar(20)String字符串类型通用
char(11)String字符类型映射为String
doubleDouble包装类可接收null值
dateDate/String可根据需求选择Date或String类型

JavaBean示例

java
public class Employee {
    private Integer eid;
    private String ename;
    private String tel;
    private String gender; //mysql中char类型映射为String
    private Double salary;
    private Double commissionPct;
    private Date birthday; //可使用Date或String
    private Date hiredate;
    private Integer jobId;
    private String email;
    private Integer mid;
    private String address;
    private String nativePlace;
    private Integer did;

    //必须提供无参构造器(反射需要)
    public Employee() {}

    //getter和setter方法
    //toString方法
}

DAO 和增删改查通用方法-BasicDAO

问题分析

apache-dbutils + Druid简化了JDBC开发,但仍有不足:

  1. SQL语句固定,不能通过参数传入,通用性不好。
  2. 对于select操作,返回类型固定,需要使用泛型。
  3. 表数量多、业务复杂时,单个Java类无法满足需求。

解决方案:设计BasicDAO通用类,作为所有DAO的父类,封装通用的CRUD操作。

基本说明

  1. DAO:Data Access Object(数据访问对象),专门负责数据库(表)的CRUD操作。
  2. BasicDAO:通用DAO类,封装所有DAO的共同操作,子类(如ActorDAOGoodsDAO)可继承并实现特有业务。
  3. 设计规范:一张表对应一个JavaBean(domain/pojo)和一个DAO子类。

BasicDAO应用实例

项目结构
com.hspedu.dao_
├── utils          // 工具类(JDBCUtilsByDruid)
├── domain         // JavaBean(Actor、Goods等)
├── dao            // DAO类(BasicDAO、ActorDAO等)
└── test           // 测试类(TestDAO)
JavaBean(Actor.java)
java
package com.hspedu.dao_.domain;

import java.util.Date;

/**
 * @author 韩顺平
 * @version 1.0
 * Actor 对象和actor 表的记录对应(JavaBean/POJO/Domain)
 */
public class Actor {
    private Integer id;
    private String name;
    private String sex;
    private Date borndate;
    private String phone;

    //无参构造器(必须,反射需要)
    public Actor() {}

    //有参构造器
    public Actor(Integer id, String name, String sex, Date borndate, String phone) {
        this.id = id;
        this.name = name;
        this.sex = sex;
        this.borndate = borndate;
        this.phone = phone;
    }

    //getter和setter方法
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getSex() { return sex; }
    public void setSex(String sex) { this.sex = sex; }
    public Date getBorndate() { return borndate; }
    public void setBorndate(Date borndate) { this.borndate = borndate; }
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }

    //toString方法
    @Override
    public String toString() {
        return "\nActor{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", borndate=" + borndate +
                ", phone='" + phone + '\'' +
                '}';
    }
}
工具类(JDBCUtilsByDruid.java)

同25.11.6中的工具类,略。

通用DAO(BasicDAO.java)
java
package com.hspedu.dao_.dao;

import com.hspedu.dao_.utils.JDBCUtilsByDruid;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

/**
 * @author 韩顺平
 * @version 1.0
 * 开发BasicDAO,是其他DAO的父类,使用apache-dbutils
 * @param <T> 泛型,指定DAO操作的JavaBean类型
 */
public class BasicDAO<T> {
    //QueryRunner线程安全,可复用
    private QueryRunner qr = new QueryRunner();

    //通用的dml方法(insert、update、delete),针对任意表
    public int update(String sql, Object... parameters) {
        Connection connection = null;
        try {
            connection = JDBCUtilsByDruid.getConnection();
            return qr.update(connection, sql, parameters);
        } catch (SQLException e) {
            //将编译异常转为运行异常抛出
            throw new RuntimeException(e);
        } finally {
            //关闭连接(放回连接池)
            JDBCUtilsByDruid.close(null, null, connection);
        }
    }

    //查询多行记录,返回List<T>,针对任意表
    public List<T> queryMulti(String sql, Class<T> clazz, Object... parameters) {
        Connection connection = null;
        try {
            connection = JDBCUtilsByDruid.getConnection();
            //BeanListHandler将resultset封装为List<T>
            return qr.query(connection, sql, new BeanListHandler<>(clazz), parameters);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCUtilsByDruid.close(null, null, connection);
        }
    }

    //查询单行记录,返回T对象,针对任意表
    public T querySingle(String sql, Class<T> clazz, Object... parameters) {
        Connection connection = null;
        try {
            connection = JDBCUtilsByDruid.getConnection();
            //BeanHandler将resultset封装为单个T对象
            return qr.query(connection, sql, new BeanHandler<>(clazz), parameters);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCUtilsByDruid.close(null, null, connection);
        }
    }

    //查询单行单列值(如count、sum等),返回Object
    public Object queryScalar(String sql, Object... parameters) {
        Connection connection = null;
        try {
            connection = JDBCUtilsByDruid.getConnection();
            //ScalarHandler处理单行单列结果
            return qr.query(connection, sql, new ScalarHandler(), parameters);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCUtilsByDruid.close(null, null, connection);
        }
    }
}
具体DAO(ActorDAO.java)
java
package com.hspedu.dao_.dao;

import com.hspedu.dao_.domain.Actor;

/**
 * @author 韩顺平
 * @version 1.0
 * ActorDAO:专门操作actor表,继承BasicDAO
 */
public class ActorDAO extends BasicDAO<Actor> {
    //继承BasicDAO的所有通用方法
    //可根据业务需求添加特有方法
}
测试类(TestDAO.java)
java
package com.hspedu.dao_.test;

import com.hspedu.dao_.dao.ActorDAO;
import com.hspedu.dao_.domain.Actor;
import org.junit.jupiter.api.Test;
import java.util.List;

/**
 * @author 韩顺平
 * @version 1.0
 * 测试ActorDAO对actor表的CRUD操作
 */
public class TestDAO {

    @Test
    public void testActorDAO() {
        ActorDAO actorDAO = new ActorDAO();

        //1. 查询多行记录
        List<Actor> actors = actorDAO.queryMulti("select * from actor where id >= ?", Actor.class, 1);
        System.out.println("===查询结果===");
        for (Actor actor : actors) {
            System.out.println(actor);
        }

        //2. 查询单行记录
        Actor actor = actorDAO.querySingle("select * from actor where id = ?", Actor.class, 6);
        System.out.println("====查询单行结果====");
        System.out.println(actor);

        //3. 查询单行单列值
        Object o = actorDAO.queryScalar("select name from actor where id = ?", 6);
        System.out.println("====查询单行单列值===");
        System.out.println(o);

        //4. 执行dml操作(insert)
        int update = actorDAO.update("insert into actor values(null, ?, ?, ?, ?)", "张无忌", "男", "2000-11-11", "999");
        System.out.println(update > 0 ? "执行成功" : "执行没有影响表");
    }
}

课后练习

开发GoodsDaoGoods类,完成对goods表的CRUD操作。

goods表结构

字段名类型约束
idint(11)主键,自增
goods_namevarchar(10)非空
pricedouble可空

测试数据

sql
insert into goods values
(null, '华为手机', 2000),
(null, '苹果手机', 3000),
(null, '小米手机', 2000),
(null, 'vivo手机', 2300),
(null, '三星手机', null),
(null, 'IBM手机', 5000),
(null, '格力手机', 1800),
(null, '海尔手机', null);